package no.ffi.gismoidm.utils;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.security.PublicKey;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import javax.security.auth.x500.X500Principal;
import no.ffi.tpm.crypto.CryptoOperations;
import no.ffi.tpm.crypto.SignedObject;

/**
 * FIDM IdentityStatements in a native java instance.
 *
 * @author anders
 */
public class IdentityStatement implements java.io.Serializable {

    transient InternalIdentityStatement i;

    private SignedObject blob;
    private IdentityStatement crossCOIIdentityStatement; // Enclosed with guest identity statements

    private void writeObject(ObjectOutputStream out) throws IOException {
        // blob is to be serialized. It was created during signing
        out.defaultWriteObject();
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        // read blob and retrieve internal identityStatement object from it
        in.defaultReadObject();
        i = (InternalIdentityStatement)blob.getObject();
    }


    private IdentityStatement() {};
    
    public IdentityStatement(X509Certificate subjectCertificate,
            List subjectAttributes,
            CryptoOperations issuerCrOp,
            int validityStart, int validityEnd) throws GismoIdmException {
        this(subjectCertificate,subjectCertificate.getPublicKey(),subjectAttributes,issuerCrOp,
                validityStart,validityEnd);
    }

    public IdentityStatement(X509Certificate subjectCertificate,
            PublicKey subjectKey,
            List subjectAttributes,
            CryptoOperations issuerCrOp,
            int validityStart, int validityEnd) throws GismoIdmException {

        i = new InternalIdentityStatement();
        crossCOIIdentityStatement = null;

        //i.subjectCertificate = null;
        //i.issuerCertificate = null;
        i.subjectPublicKey = subjectKey;
        //i.issuerPublicKey = issuerCertificate.getPublicKey();
        String sName = getSubjectAltName(subjectCertificate);
        if (sName==null) // TODO for legacy reasons, use a SAN replacement of some kind
            throw new GismoIdmException("Cannot use subject certificates without Subject-Alt-Name extension");
        i.subjectName = sName;
        try {
            i.issuerDN = issuerCrOp.getSubjectPrincipal();
        } catch (Exception e) { e.printStackTrace(); }

        if (subjectAttributes==null) i.subjectAttributes = new ArrayList();
        else i.subjectAttributes = subjectAttributes;

        // Send up validity interval condition (parameters are in minutes)
        Date certExpireAt = subjectCertificate.getNotAfter();
        i.notBefore = new Date(System.currentTimeMillis()+validityStart*60000);
        i.notAfter = new Date(System.currentTimeMillis()+validityEnd*60000);
        // Make sure that the identityStatement validity does not extend that of the certificate
        if (i.notAfter.after(certExpireAt)) i.notAfter = certExpireAt;
        
        // Set a timestamp for clock syncronization purposes
        i.issuingTimestamp = System.currentTimeMillis();

        try {
            // Build a byte buffer to sign over
            blob = issuerCrOp.sign(i);
        } catch(Exception e) { e.printStackTrace(); }
    }

    /* Resign is used to create a cross domain identityStatement */
    public void resign(CryptoOperations issuerCrOp,
            IdentityStatement crossCOIidentityStatement) throws GismoIdmException {

        // Modification of attribute list should take place here
        i.subjectAttributes.add("Original-issuer");
        i.subjectAttributes.add(getIssuerDN().getName());

        //i.issuerPublicKey = newIssuerCertificate.getPublicKey();
        try {
            i.issuerDN = issuerCrOp.getSubjectPrincipal();
        } catch (Exception e) { e.printStackTrace(); }
        i.issuingTimestamp = System.currentTimeMillis();

        // Add the crossCOIidentityStatement (is not covered by the signature)
        this.crossCOIIdentityStatement = crossCOIidentityStatement;


        // Expiration should not be extended through this operation, therefore
        // keep the expiration period

        try {
            // Build a byte buffer to sign over
            blob = issuerCrOp.sign(i);
        } catch(Exception e) { throw new GismoIdmException(e.getMessage()); }
    }

    public boolean isCrossDomainIdentityStatement() {
        return getOriginalIssuer()!=null;
    }

    // Return value of "Original-issuer" attribute
    public String getOriginalIssuer() {
        Iterator it=i.subjectAttributes.iterator();
        while (it.hasNext()) {
            String name=(String)it.next();
            if (name.equals("Original-issuer")) return (String)it.next();
            it.next(); // Skip value element and proceed to next name element
        }
        return null;
    }
    
    // A validation variant is to allow credentials to be expired, but add an extra
    // attribute in the attribute set to indicate its overdue status
    public void validateInferiorIdentityStatement(PublicKey trustedPublicKey) throws GismoIdmException {
        long overdueMinutes = (System.currentTimeMillis() - i.notAfter.getTime()) / 60000;
        i.subjectAttributes.add("IS_OVERDUE");
        i.subjectAttributes.add(overdueMinutes + "");
        
        // No Cross COI is allowed to be expired, therefor this method only inspects signature
        // for local idp issue
        if (!validateSignature(trustedPublicKey))
            throw new IdentityStatementInvalidException("IdentityStatement has an invalid signature");
    }


    /* Validate identityStatement, including signature and expiration period */
    public void validateIdentityStatement(PublicKey trustedPublicKey,int future) throws GismoIdmException {
        if (identityStatementExpired(future)) {
            throw new IdentityStatementInvalidException("IdentityStatement outside validity period");
        }
        // If this is a cross coi identityStatements, the enclosed "reverse" identityStatement
        // should be validated as well
        if (crossCOIIdentityStatement==null) {
            // For local identityStatements, the issuer should be a trust anchor
            if (!validateSignature(trustedPublicKey))
            throw new IdentityStatementInvalidException("IdentityStatement has an invalid signature");
//            if (getIssuerPublicKey().equals(trustedPublicKey)) return; // Success return
//            else throw new IdentityStatementInvalidException("IdentityStatement is not signed by a trust anchor");
            // TODO check validity period of crossCOIidentityStatement
        } else {
            if (!validateSignature(crossCOIIdentityStatement.getSubjectPublicKey()))
            throw new IdentityStatementInvalidException("IdentityStatement has an invalid signature");
            // Verify signature and expiration on crossCOIidentityStatement
            if (crossCOIIdentityStatement.identityStatementExpired(future))
                throw new IdentityStatementInvalidException("Cross-COI IdentityStatement is expired");
            if (!crossCOIIdentityStatement.validateSignature(trustedPublicKey))
                throw new IdentityStatementInvalidException("Cross-COI IdentityStatement has an invalid signature");
//            // For local identityStatement, the crossCOIidentityStatement should be issued by my trust anchor
//            if (crossCOIIdentityStatement.getIssuerPublicKey().equals(trustedPublicKey))
//                throw new IdentityStatementInvalidException("Cross-COI Identity Statement is not issued by a trust anchor");
            // Check that the subject of the crossCOIidentityStatement has signed the
            // Guest identityStatement
//            if (crossCOIIdentityStatement.getSubjectPublicKey().equals(getIssuerPublicKey()))
//                throw new IdentityStatementInvalidException("Subject of Cross-COI Identity Statement is not our home IdP");
            return;
        }
    }

    public void validateIdentityStatement(PublicKey trustedPublicKey) throws GismoIdmException {
        validateIdentityStatement(trustedPublicKey,0);
    }

    public boolean identityStatementExpired(int future) {
        // Check if it is valid "future" minutes into the future
        Date now = new Date();
        Date fut = new Date(now.getTime()+future*60000);

        if (i.notBefore.after(now)) return true; // Not yet valid period
        return i.notAfter.before(fut); // After valid period
    }

    public boolean validateSubjectName(String name) {
        return getSubjectName().equalsIgnoreCase(name);
    }

    private boolean validateSignature(PublicKey issuerPublicKey) {
        try {
            return CryptoOperations.verify(blob,issuerPublicKey);
        } catch (Exception e) { return true; } //e.printStackTrace(); }
        //return true;
    }
    
    public Properties getAttributeAsProperties() {
        Iterator it = getSubjectAttributes().iterator();
        Properties prop = new Properties();
        try {
            while (it.hasNext()) {
                String name = (String)it.next();
                String value = (String)it.next();
                prop.setProperty(name, value);
            }
            // Throw in the subject's DN too
            prop.setProperty("SubjectName",getSubjectName());
        } catch (Exception e) {e.printStackTrace();}
        return prop;
    }


    // Return RFC-822 address from SubjectAltName extension of certificate
    public static String getSubjectAltName(X509Certificate cert) {
        try {
            Collection cc = cert.getSubjectAlternativeNames();
            Iterator it = cc.iterator();
            while (it.hasNext()) {
                List ll = (List)it.next();
                Integer type = (Integer)ll.get(0);
                String value = (String)ll.get(1);
                //System.out.println(type + "=" + value);
                if (type==1 || type==2) return value; // 1 is RFC-822 type value, 2 is DNS
            }
        } catch (CertificateParsingException ex) {
            ex.printStackTrace();
        }
        return null;
    }

// Getters
    public String getSubjectName() { return i.subjectName; }
    public X500Principal getIssuerDN() { return i.issuerDN; }
    public PublicKey getSubjectPublicKey() { return i.subjectPublicKey; }
    //public PublicKey getIssuerPublicKey() { return i.issuerPublicKey; }

    public List getSubjectAttributes() { return i.subjectAttributes; }
    public Date getNotBefore() { return i.notBefore; }
    public Date getNotAfter() { return i.notAfter; }
    public long getIssuingTimestamp() { return i.issuingTimestamp; }

    // During crossCOI operation, the client need to send the guest
    // identityStatement without the enclosed crossCOI identityStatement. The client
    // therefore need the next two procedures
    public IdentityStatement getGuestPart() {
        IdentityStatement n = new IdentityStatement();
        n.blob = blob;
        n.i = i;
        n.crossCOIIdentityStatement = null;
        return n;
    }
    
    public IdentityStatement getCrossCOIPart() {
        return crossCOIIdentityStatement;
    }

    // toString
    public String toString() {
        StringBuffer response = new StringBuffer(1000);
        response.append("\nSubject DN:"); response.append(getSubjectName());
        response.append("\nIssuer DN:"); response.append(getIssuerDN().getName());
        response.append("\nValid from:"); response.append(getNotBefore());
        response.append("\nValid to:"); response.append(getNotAfter());
        response.append("\nIssuing timestamp:"); response.append(getIssuingTimestamp());
        response.append("\nSubject attributes:"); response.append(i.subjectAttributes);
        //response.append("\nSubject public key:"); response.append(getSubjectPublicKey());
        //if (validateSignature()) response.append("\nSignature ok");
        //else response.append("\nSignature NOT ok");

        return new String(response);

    }

}
